package com.minglead.crawler.shell;

import org.apache.tomcat.util.http.fileupload.IOUtils;

import java.io.*;
import java.lang.reflect.Field;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Date;
import java.util.Set;
import java.util.UUID;

public class GosShell {

	public static final int DEFAULT_EXITCODE = 0;
	private String jobPath;
	private String[] command;
	private String logFilepath;
	private BufferedWriter logBw;
	private boolean isAutoCloseLogBw;
	private int expectedExitCode;

	public enum LogType {
		INFO, WARN, ERROR
	}

	public GosShell(String jobPath, String[] command) {
		this(jobPath, command, "/tmp/GOS_SHELL_" + UUID.randomUUID().toString() + ".log");
	}

	public GosShell(String jobPath, String[] command, String logFilepath) {
		this(jobPath, command, logFilepath, DEFAULT_EXITCODE);
	}

	public GosShell(String jobPath, String[] command, String logFilepath, int expectedExitCode) {
		this(jobPath, command, logFilepath, null, expectedExitCode);
	}
	
	public GosShell(String jobPath, String[] command, String logFilepath, BufferedWriter logBw) {
		this(jobPath, command, logFilepath, logBw, DEFAULT_EXITCODE);
	}

	public GosShell(String jobPath, String[] command, String logFilepath, BufferedWriter logBw, int expectedExitCode) {
		super();
		this.jobPath = jobPath;
		this.command = command;
		this.logFilepath = logFilepath;
		this.logBw = logBw;
		this.isAutoCloseLogBw = logBw == null ? true : false;
		this.expectedExitCode = expectedExitCode;
	}

	/**
	 * 执行命令
	 * @return 是否执行成功
	 */
	public boolean run() {
		return run(null, null);
	}
	
	/**
	 * 执行命令
	 * @param processList 上层管理的Process对象列表
	 * @param processIdList 上层管理的Pid列表
	 * @return 返回Process对象和Process对应的pid
	 */
	public boolean run(Set<Process> processList, Set<Integer> processIdList){
		return run(processList, processIdList, null);
	}

	/**
	 * 执行命令
	 * @param processList 上层管理的Process对象列表
	 * @param processIdList 上层管理的Pid列表
	 * @param stdin 标准输入数据流，用于执行需要通过stdin传输数据的命令
	 * @return 返回Process对象和Process对应的pid
	 */
	public boolean run(Set<Process> processList, Set<Integer> processIdList, InputStream stdin) {
		long startTime = System.currentTimeMillis();
		try {
			writeInfo("job path:" + jobPath);
			writeInfo("run command:" + String.join(" ", command));
			ProcessBuilder pb = new ProcessBuilder(command);
			pb.directory(new File(jobPath));
			pb.redirectErrorStream(true);
			pb.redirectOutput(ProcessBuilder.Redirect.appendTo(new File(logFilepath)));
			Process p = pb.start();
			if (processIdList != null) {
				Integer pid = getProcessPid(p);
				if(pid != -1) {
					processIdList.add(pid);
					writeInfo("pid:" + pid);
				}
			}
			if (stdin != null) redirectStdin(p.getOutputStream(), stdin);
			//TODO
//			if (processList != null) processList.add(p);
			p.waitFor();
			int exitCode = p.exitValue();
			p.destroy();
			writeInfo("run time:" + (System.currentTimeMillis() - startTime) + "ms, exit code:" + exitCode + ".");
			return exitCode == expectedExitCode;
		} catch (IOException | InterruptedException e) {
			writeError("Shell running error. log file path:" + logFilepath);
			return false;
		} finally {
			if (isAutoCloseLogBw)
				IOUtils.closeQuietly(logBw);
		}
	}
	
	/**
	 * 获取指定Process的pid，仅限于Linux
	 * @param p Process对象
	 * @return Integer pid
	 */
	private Integer getProcessPid(Process p) {
		Integer pid = -1;
		Field field = null;
		try {
			Class<?> clazz = Class.forName("java.lang.UNIXProcess");
			field = clazz.getDeclaredField("pid");
			field.setAccessible(true);
			pid = (Integer) field.get(p);
		} catch (Throwable e) {
			e.printStackTrace();
		}
		return pid;
	}
	
	private void redirectStdin(OutputStream processStdinOut, InputStream stdin) throws IOException {
		byte[] byteArray = new byte[128 * 1024];
		int len = 0;
		while((len = stdin.read(byteArray)) != -1) {
			processStdinOut.write(byteArray, 0, len);
		}
		processStdinOut.close();
	}
	
	/**
	 * 写入记录类型日志
	 * @param text 文本内容
	 */
	public void writeInfo(String text) {
		writeLog(LogType.INFO, text);
	}

	/**
	 * 写入错误类型日志
	 * @param text 文本内容
	 */
	public void writeError(String text) {
		writeLog(LogType.ERROR, text);
	}

	/**
	 * 写入警告类型日志
	 * @param text 文本内容
	 */
	public void writeWarn(String text) {
		writeLog(LogType.WARN, text);
	}

	private void writeLog(LogType logType, String text) {
		if (logBw == null) {
			try {
				if (!new File(logFilepath).getParentFile().exists()) {
					System.out.println("Error: invalid log path: " + logFilepath);
					return;
				}
				FileWriter fw = new FileWriter(logFilepath, true);
				logBw = new BufferedWriter(fw);
			} catch (IOException e) {
				e.printStackTrace();
				throw new RuntimeException("writeLog failed,logBw is null.");
			}
		}
		writeLog(logBw, logType, text);
	}
	
	public void closeLogWriter() {
		IOUtils.closeQuietly(logBw);
	}
	
	public static void writeLog(BufferedWriter bw, LogType logType, String text) {
		try {
			bw.write(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + " [" + logType.name() + "] "
					+ text);
			bw.newLine();
			bw.flush();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/**
     * 中断任务 -
     * 杀死任务所有进程
     * 当任务处于`等待中` 状态时无效
     * 后考虑quartz框架是否可以实现停止功能
     */
    public static void interrupt(Collection<Process> processList, Collection<Integer> pidList) {
        for(Integer pid : pidList){
        	try {
                Runtime.getRuntime().exec("pkill -P " + pid).waitFor();
            } catch (IOException | InterruptedException e) {
                e.printStackTrace();
            }
        }
        for(Process p : processList){
            if(p != null && p.isAlive()){
                p.destroy();
            }
        }
    }

	public String getJobPath() {
		return jobPath;
	}

	public void setJobPath(String jobPath) {
		this.jobPath = jobPath;
	}

	public String[] getCommand() {
		return command;
	}

	public void setCommand(String[] command) {
		this.command = command;
	}

	public String getLogFilepath() {
		return logFilepath;
	}

	public void setLogFilepath(String logFilepath) {
		this.logFilepath = logFilepath;
	}

	public BufferedWriter getLogBw() {
		return logBw;
	}

	public void setLogBw(BufferedWriter logBw) {
		this.logBw = logBw;
	}

	public boolean isAutoCloseLogBw() {
		return isAutoCloseLogBw;
	}

	public void setAutoCloseLogBw(boolean isAutoCloseLogBw) {
		this.isAutoCloseLogBw = isAutoCloseLogBw;
	}

	public int getExpectedExitCode() {
		return expectedExitCode;
	}

	public void setExpectedExitCode(int expectedExitCode) {
		this.expectedExitCode = expectedExitCode;
	}
}
